Struts2 RCE漏洞分析系列 - 0x01基础篇

众所周知,Java Web中使用最广泛的两个mvc框架,一是Spring MVC,一是Struts2。
但Struts由于安全性、性能、内存消耗等短板,已经越来越少新项目使用它了,更多的开发人员钟爱于Spring MVC。

说起Struts2的安全问题,到目前为止,已经被爆出两位数以上的RCE(远程命令执行)漏洞了。而很多国企、政府、小企业部署的web系统,一般出于缺乏技术研发人员,很多项目都是外包到外包企业,因此大量的旧项目采用了Struts2框架,并由于缺少资金投入,年久失修,经常性一个漏洞被爆出后,三五个月都没有去修复,使得黑客乘虚而入,盗取大量用户信息。

目前为止已经公布的Struts2 RCE漏洞以及影响版本列表:

Struts2 S2-001

1
2
影响版本:2.0.0 - 2.0.8
具体详情:https://struts.apache.org/docs/s2-001.html

Struts2 S2-005

1
2
影响版本: 2.0.0 - 2.1.8.1
漏洞详情: http://struts.apache.org/docs/s2-005.html

Struts2 S2-009

1
2
影响版本: 2.0.0 - 2.3.1.1
漏洞详情: https://struts.apache.org/docs/s2-009.html

Struts2 S2-012

1
2
影响版本: 2.0.0 - 2.3.13
漏洞详情: https://cwiki.apache.org/confluence/display/WW/S2-012

Struts2 S2-013/S2-014

1
2
影响版本: 2.0.0 - 2.3.14.1
漏洞详情: https://cwiki.apache.org/confluence/display/WW/S2-013,https://cwiki.apache.org/confluence/display/WW/S2-014

Struts2 S2-016

1
2
影响版本: 2.0.0 - 2.3.15
漏洞详情: https://struts.apache.org/docs/s2-016.html

Struts2 S2-019

1
2
影响版本: 2.0.0 - 2.3.15.1
漏洞详情: https://cwiki.apache.org/confluence/display/WW/S2-019

Struts2 S2-032

1
2
影响版本: 2.3.20 - 2.3.28 (except 2.3.20.3 and 2.3.24.3)
漏洞详情: https://struts.apache.org/docs/s2-032.html

Struts2 S2-037

1
2
影响版本: 2.3.20 - 2.3.28.1
漏洞详情: http://struts.apache.org/docs/s2-037.html

Struts2 S2-045

1
2
影响版本:2.3.5-2.3.31
影响版本:2.5-2.5.10

Struts2 S2-048

1
2
影响版本:2.3.5 -2 .3.31
影响版本:2.5 - 2.5.10

Struts2 S2-052

1
2
影响版本:2.1.2 - 2.3.33
影响版本:2.5 - 2.5.12

Struts2 S2-053

1
2
影响版本:2.0.1 - 2.3.33
影响版本:2.5 - 2.5.10

Struts2 S2-055

1
影响版本:2.5 - 2.5.14

Struts2 S2-057

1
影响版本:2.5 - 2.5.16

Struts2上大部分的RCE漏洞,基本都是OgnlUtil使用不当导致的,想要理解Struts漏洞的利用原理,提前学习以下知识,有助于充分理解Struts2漏洞利用原理:

  1. ValueStack,对于每一个action的调用,Struts在执行相应的动作方法之前会先创建一个名为ValueStack的对象
  2. OgnlUtil、OgnlContext
  3. OGNL(Object-Graph Navigation Language),它是一个功能强大的表达式语言,用来获取和设置 java 对象的属性 , 它旨在提供一个更高抽象度语法来对 java 对象图进行导航

ps:以下源码分析,将使用的是Struts2.5.16版本

ValueStack

对于每一个action的调用,Struts在执行相应的动作方法之前会先创建一个名为ValueStack的对象。

为了更好得研究ValueStack的结构,我们下载struts2-core.jar源码进行分析

打开web.xml,可以看到org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter就是每个http请求的入口了,查看StrutsPrepareAndExecuteFilter源码doFilter方法

image
image

跟进132行 prepare.createActionContext(request, response);

image
image
image

可以看到方法实现,在77行ActionContext.getContext()中,通过当前线程变量actionContext获取ActionContext,如果不存在ActionContext,则会创建一个新的ActionContext实例,并且在87行的ActionContext.setContext(ctx),把ActionContext保存在线程变量actionContext中,也就是说,对于每一个线程,都会有一个单独的ActionContext。

在82行中,我们可以发现,代码上使用了Dispatcher中的Container去实例化一个ValueStackFactory,ValueStackFactory是ValueStack的一个创建工厂,方法createValueStack用于创建ValueStack,而Dispatcher中的Container,对于每一个实例化注册到容器中的对象,都会实现控制反转,根据配置注入相关参数、依赖,在payload中,我们将会用到该容器对象,进行实例化OgnlUtil以实现执行RCE,继续跟进82行中ValueStackFactory的createValueStack方法

image

从方法中可以看到,实例化了一个OgnlValueStack对象,我们根进该对象,查看对象具体信息

image

从OgnlValueStack类信息中,可以看到有一个名为root的CompoundRoot对象,以及一个名为context的Map<String, Object>对象,查看CompoundRoot可以发现,该类其实是一个List对象,根据这篇文章的描述我们可以了解到,使用OGNL访问context里的对象需要在对象名之前添加 #,访问root对象的属性时,可以省略#而直接通过属性名来搜索。

image

继续回到OgnlValueStack构造方法,跟进setRoot方法,可以看到OgnlValueStack通过context.put(VALUE_STACK, this)把它自身也put进了context这个Map中。

image

回到OgnlValueStackFactory类的createValueStack方法,这里有一句stack.getContext().put(ActionContext.CONTAINER, container),把Container对象放进了ValueStack的context的Map中,前面我们已经讲解过了,该容器Container对象,可以实现调用方法getInstance进行实例化目标对象,并注入相关依赖。

image

再回到PrepareOperations类的createActionContext方法,跟进下一行stack.getContext().putAll(dispatcher.createContextMap(request, response, null))中的createContextMap方法。

image
image

可以看到,该语句实现了把request、params、session、application、mapping全部放进了ValueStack的context中,也就是说,通过OGNL语句,我们只需要使用#加key就可以轻松取到以上对象。

else中的最后一句ctx = new ActionContext(stack.getContext()),把ValueStack的context传入到一个新的ActionContext中。

image
image

回到StrutsPrepareAndExecuteFilter类的doFilter方法,跟进execute.executeAction(request, response, mapping),接着继续跟进dispatcher.serviceAction(request, response, mapping)

image

分析上面的代码,可以看到567行的request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack())以及579行的request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack),会把ValueStack以key为struts.valueStack设置进request,也就是说,在OGNL中,我们可以通过request对象直接取得ValueStack。

OgnlUtil、OgnlContext

OgnlUtil是Struts2中处理执行OGNL的工具类,它的实现类存在于struts2-core.jar中的com.opensymphony.xwork2.ognl包底下。

打开类可以看到以下代码

image

其中有三个Set集合:

1
2
3
private Set<Class<?>> excludedClasses = Collections.emptySet();
private Set<Pattern> excludedPackageNamePatterns = Collections.emptySet();
private Set<String> excludedPackageNames = Collections.emptySet();

这三个集合可以在OgnlUtil执行OGNL的时候,对部分class、package进行排除,不允许执行,也就是一个黑名单。

image

查看这三个集合的set方法都有注解@Inject,通过容器注入了相关constant

image

跟进去三个注解的value,看到三个value值分别是:

1
2
3
4
5
6
@Deprecated
public static final String OGNL_EXCLUDED_CLASSES = "ognlExcludedClasses";
@Deprecated
public static final String OGNL_EXCLUDED_PACKAGE_NAME_PATTERNS = "ognlExcludedPackageNamePatterns";
@Deprecated
public static final String OGNL_EXCLUDED_PACKAGE_NAMES = "ognlExcludedPackageNames";

然后,我们查看struts-default.xml文件,但是,我们搜索相关constant这时候是找不到的,因为在org.apache.struts2.config.DefaultBeanSelectionProvider类的register方法中进行了转换,这三个value对应的constant分别是通过一下三个value来注入的:

1
2
3
public static final String STRUTS_EXCLUDED_CLASSES = "struts.excludedClasses";
public static final String STRUTS_EXCLUDED_PACKAGE_NAME_PATTERNS = "struts.excludedPackageNamePatterns";
public static final String STRUTS_EXCLUDED_PACKAGE_NAMES = "struts.excludedPackageNames";

image
image

回到struts-default.xml文件,搜索可以看到注入的值为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<constant name="struts.excludedClasses"
value="
java.lang.Object,
java.lang.Runtime,
java.lang.System,
java.lang.Class,
java.lang.ClassLoader,
java.lang.Shutdown,
java.lang.ProcessBuilder,
ognl.OgnlContext,
ognl.ClassResolver,
ognl.TypeConverter,
ognl.MemberAccess,
ognl.DefaultMemberAccess,
com.opensymphony.xwork2.ognl.SecurityMemberAccess,
com.opensymphony.xwork2.ActionContext" />
1
2
3
4
5
6
7
8
9
10
11
<constant name="struts.excludedPackageNames"
value="
java.lang.,
ognl.,
javax,
freemarker.core.,
freemarker.template.,
freemarker.ext.rhino.,
freemarker.ext.beans.,
sun.reflect.,
javassist." />

以及被注释掉的

1
<!-- constant name="struts.excludedPackageNamePatterns" value="^java\.lang\..*,^ognl.*,^(?!javax\.servlet\..+)(javax\..+)" / -->

回到三个方法,setExcludedClasses、setExcludedPackageNamePatterns、setExcludedPackageNames,我们都可以看到,Set集合都使用了Collections.unmodifiableSet,跟进该集合类可以发现,该类继承了UnmodifiableCollection类,分析该类的方法代码可知,该类实例化对象中存储的元素是不允许修改的。那么,有没有方法进行修改呢,其实我们不需要修改原有的Set对象,我们只需要分别重新调用一次OgnlUtil的setExcludedClasses、setExcludedPackageNamePatterns、setExcludedPackageNames方法,传入其他无关紧要的class、package,就可以新生成一个Set对象,替换OgnlUtil的引用,以达到绕过黑名单限制,实现通过OGNL执行命令。

OGNL(Object-Graph Navigation Language)

ognl的用法非常之简单,这里我就不详细描述相关语法了,贴上链接,感兴趣的可以去喵喵,建议有时间还是需要去看看的,对于后面了解payload很有帮助。
https://www.ibm.com/developerworks/cn/opensource/os-cn-ognl/index.html)